Spring AOP
1. 大白话:这是什么?解决什么痛点?
Spring AOP 的本质是:不修改核心业务代码,在方法调用前后动态织入一段通用增强逻辑。
业务方法只负责业务本身,比如下单、扣库存、查订单;而日志、权限、事务、监控、限流这些能力虽然很重要,但它们不是业务主线。如果每个业务方法里都手写一遍,代码会重复、侵入性强,后期改一处规则还要到处找。
AOP 解决的就是这种“横切逻辑污染业务代码”的问题。它把公共能力抽成切面,在符合条件的方法上统一生效。面试里可以一句话概括:AOP 是用代理机制把通用增强逻辑从业务代码里剥离出来,降低重复代码和模块耦合。
2. 底层机制与高频考点
AOP 的术语要按“在哪里增强、增强什么、怎么织进去”来记:
- JoinPoint(连接点):目标类里所有可能被增强的方法。
- Pointcut(切入点):真正被匹配、被增强的方法。切入点一定是连接点,连接点不一定是切入点。
- Advice(通知):真正执行的增强逻辑,比如开启事务、打印日志。
- Aspect(切面):
Pointcut + Advice,也就是“在哪些方法上执行哪些增强”。 - Target(目标对象):原始业务对象。
- Proxy(代理对象):Spring 给目标对象生成的代理,外部实际调用的是它。
- Weaving(织入):把通知应用到目标对象并生成代理对象的过程。
Spring AOP 的底层是运行时动态代理。Spring 容器创建 Bean 时,如果发现它需要被切面增强,就会创建代理对象放进容器里。后续外部调用 Bean 方法时,先进入代理对象,再由代理对象执行通知逻辑,最后决定是否调用目标方法。
代理选择规则:
| 场景 | 默认代理方式 | 原理 | 限制 |
|---|---|---|---|
| 目标类实现接口 | JDK 动态代理 | 生成接口实现类,调用进入 InvocationHandler#invoke | 必须有接口 |
| 目标类没有接口 | CGLIB 动态代理 | 生成目标类子类,调用进入方法拦截器 | final 类、final/private 方法无法增强 |
常见通知类型:
- Before:目标方法执行前触发。
- After:目标方法执行后触发,不管成功还是异常都会执行。
- AfterReturning:目标方法正常返回后触发。
- AfterThrowing:目标方法抛异常后触发。
- Around:环绕通知,能力最强,可以在目标方法前后做增强,也可以选择不调用目标方法。
多个切面的顺序通常用 @Order 或实现 Ordered 接口控制。数值越小,优先级越高;环绕通知的进入顺序和退出顺序可以理解成“先进后出”。
Spring AOP 和 AspectJ 的区别:
| 对比点 | Spring AOP | AspectJ |
|---|---|---|
| 增强时机 | 运行时增强 | 编译期或类加载期增强 |
| 底层方式 | 动态代理 | 字节码织入 |
| 能增强什么 | 主要是 Spring Bean 的方法级调用 | 方法、字段、构造器、静态方法等更广 |
| 使用复杂度 | 简单,和 Spring 集成好 | 配置更复杂 |
| 典型选择 | 常规业务日志、事务、权限 | 非 Spring 对象、更复杂切点、高性能大量切面 |
最高频坑点是自调用失效。因为 Spring AOP 依赖代理对象,只有外部通过代理对象调用方法时,切面才会生效。如果同一个类内部用 this.method() 调用另一个带 @Transactional 的方法,这次调用直接发生在原始对象内部,没有经过代理对象,所以事务、日志等增强都会失效。
事务为什么依赖 AOP?因为声明式事务 @Transactional 的底层就是 AOP 动态代理。调用事务方法时,代理对象会进入 TransactionInterceptor,在目标方法执行前开启事务,方法异常时回滚,正常结束后提交。也就是说,事务能不能生效,关键看调用链有没有经过 Spring 生成的代理对象。
3. 🎯 实战口径
在我的项目里,Spring AOP 最典型的落点是 [[黑马点评]] 的秒杀异步下单事务。
秒杀请求先通过 Redis Lua 完成库存预扣减和一人一单校验,然后把订单消息写入 Redis Stream,由异步线程消费并真正创建订单。创建订单的方法需要 @Transactional 保证查重、扣库存、落库这些数据库操作的一致性。但这里有一个坑:如果在同一个 Service 内部直接用 this.createVoucherOrder() 调用事务方法,就不会经过 Spring 代理对象,TransactionInterceptor 进不来,事务会失效。
所以我的处理是:在主线程中通过 AopContext.currentProxy() 提前拿到当前 Service 的代理对象,保存后给异步消费线程使用;异步线程里通过 proxy.createVoucherOrder() 调用事务方法,让调用链真正进入 Spring AOP 代理。这样 @Transactional 才能生效。
同时,我把 Redisson 分布式锁包在代理调用外层,而不是写在事务方法内部。这样可以保证事务执行完成后再释放锁,避免“锁释放了,但事务还没提交,其他线程又进来读到旧数据”的并发问题。这个点面试时要讲清楚:我不是为了形式上用代理,而是为了保证 代理拦截、事务边界、锁释放顺序 都在正确的位置。
在 [[苍穹外卖AI客服]] 里,Spring AI 的 Advisor 链也可以类比 AOP 思想来讲。比如 SafeToolCallAdvisor 会在模型继续调用工具前先做拦截,检查工具调用轮次和重复签名;如果发现工具调用死循环,就短路返回兜底响应。它不是传统 Spring AOP,但思想是一致的:把安全控制、缓存、记忆注入、工具过滤这些横切能力放到统一链路里,不污染核心业务逻辑。
相关链接:[[代理模式]] | [[苍穹外卖AI客服]] | [[黑马点评]]